package com.gitee.wsl.compose.modifier.animate

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.sin
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.gitee.wsl.compose.modifier.ModifierAnimateScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

enum class DismissDirection {
    Start, End, Up, Down
}

@Composable
fun rememberDismissibleState(
    onDismiss: DismissibleState.(DismissDirection) -> Unit = {},
    onDismissCancel: () -> Unit = {}
): DismissibleState {
    val layoutDirection = LocalLayoutDirection.current
    val onDismissState = rememberUpdatedState(onDismiss)
    val onDismissCancelState = rememberUpdatedState(onDismissCancel)
    return remember {
        DismissibleState(
            layoutDirection,
            { onDismissState.value.invoke(this, it) },
            { onDismissCancelState.value.invoke() }
        )
    }
}

@Stable
class DismissibleState(
    private val layoutDirection: LayoutDirection,
    val onDismiss: DismissibleState.(DismissDirection) -> Unit,
    val onDismissCancel: () -> Unit
) {
    private val offset = Animatable(Offset.Zero, Offset.VectorConverter)

    internal var directions: Set<DismissDirection> by mutableStateOf(emptySet())
    internal var containerWidth: Float by mutableStateOf(0f)
    internal var containerHeight: Float by mutableStateOf(0f)
    internal var maxRotationZ: Float by mutableStateOf(0f)
    internal var velocityThreshold: Float by mutableStateOf(0f)
    internal var minHorizontalProgressThreshold: Float by mutableStateOf(0f)
    internal var minVerticalProgressThreshold: Float by mutableStateOf(0f)

    val value get() = offset.value
    val targetValue get() = offset.targetValue

    /**
     * Not coerced by directions
     */
    val horizontalDismissProgress by derivedStateOf {
        offset.value.x / containerWidth * if (layoutDirection == LayoutDirection.Rtl) -1 else 1
    }

    /**
     * Not coerced by directions
     */
    val verticalDismissProgress by derivedStateOf {
        offset.value.y / containerHeight
    }

    val rotationZ by derivedStateOf {
        maxRotationZ * offset.value.x / containerWidth
    }

    /**
     * The [DismissDirection] the composable was swiped at.
     *
     * Null value means the composable has not been swiped fully yet.
     */
    var dismissedDirection: DismissDirection? by mutableStateOf(null)
        private set

    private val endX by derivedStateOf {
        getEndX(containerWidth = containerWidth, containerHeight = containerHeight).toFloat()
    }
    private val endY by derivedStateOf {
        getEndY(containerWidth = containerWidth, containerHeight = containerHeight).toFloat()
    }

    suspend fun reset(
        animationSpec: AnimationSpec<Offset>? = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    ) {
        dismissedDirection = null
        if (animationSpec != null) {
            offset.animateTo(Offset.Zero, animationSpec)
        } else {
            offset.snapTo(Offset.Zero)
        }
    }

    suspend fun dismiss(direction: DismissDirection, spec: AnimationSpec<Offset> = tween(500)) {
        val directionMultiplier = if (layoutDirection == LayoutDirection.Rtl) -1 else 1
        when (direction) {
            DismissDirection.Start -> offset.animateTo(
                offset(x = -endX * directionMultiplier),
                spec
            )

            DismissDirection.End -> offset.animateTo(offset(x = endX * directionMultiplier), spec)
            DismissDirection.Up -> offset.animateTo(offset(y = -endY), spec)
            DismissDirection.Down -> offset.animateTo(offset(y = endY), spec)
        }
        this.dismissedDirection = direction
        onDismiss(direction)
    }

    internal suspend fun performFling(velocity: Velocity) {
        val directionMultiplier =
            if (layoutDirection == LayoutDirection.Rtl) -1 else 1

        val coercedVelocity = velocity.coerceIn(
            allowedDirections = directions
        )
        val dismissDirectionDueToVelocity = getDismissDirection(
            valueX = coercedVelocity.x * directionMultiplier,
            valueY = coercedVelocity.y,
            minValueX = velocityThreshold,
            minValueY = velocityThreshold,
        )

        val coercedOffset = offset.targetValue.coerceIn(
            allowedDirections = directions,
            maxWidth = containerWidth,
            maxHeight = containerHeight
        )
        val dismissDirectionDueToOffset = getDismissDirection(
            valueX = coercedOffset.x * directionMultiplier,
            valueY = coercedOffset.y,
            minValueX = containerWidth * minHorizontalProgressThreshold,
            minValueY = containerHeight * minVerticalProgressThreshold,
        )

        val dismissDirection: DismissDirection? =
            dismissDirectionDueToVelocity ?: dismissDirectionDueToOffset
        if (dismissDirection != null) {
            dismiss(dismissDirection)
        } else {
            reset()
        }
    }

    internal suspend fun performDrag(dragged: Offset) {
        val original = offset.targetValue
        val summed = original + dragged
        offset.animateTo(
            offset(
                x = summed.x.coerceIn(-containerWidth, containerWidth),
                y = summed.y.coerceIn(-containerHeight, containerHeight)
            )
        )
    }

    private fun offset(x: Float = offset.value.x, y: Float = offset.value.y): Offset {
        return Offset(x, y)
    }

    private fun getEndX(containerWidth: Float, containerHeight: Float): Double {
        val maxRotationsRadians = Math.toRadians(maxRotationZ.toDouble())
        val ninetyDegreesRadians = Math.toRadians(90.0)
        return abs(containerWidth * sin(ninetyDegreesRadians - maxRotationsRadians)) +
                abs(containerHeight * sin(maxRotationsRadians))
    }

    private fun getEndY(containerWidth: Float, containerHeight: Float): Double {
        val maxRotationsRadians = Math.toRadians(maxRotationZ.toDouble())
        val ninetyDegreesRadians = Math.toRadians(90.0)
        return abs(containerHeight * sin(ninetyDegreesRadians - maxRotationsRadians)) +
                abs(containerWidth * sin(maxRotationsRadians))
    }

    private fun Offset.coerceIn(
        allowedDirections: Set<DismissDirection>,
        maxWidth: Float,
        maxHeight: Float,
    ): Offset = copy(
        x = x.coerceWidthIn(allowedDirections, maxWidth),
        y = y.coerceHeightIn(allowedDirections, maxHeight)
    )

    private fun Velocity.coerceIn(
        allowedDirections: Set<DismissDirection>
    ): Velocity = copy(
        x = x.coerceWidthIn(allowedDirections, Float.MAX_VALUE),
        y = y.coerceHeightIn(allowedDirections, Float.MAX_VALUE)
    )

    private fun Float.coerceWidthIn(
        allowedDirections: Set<DismissDirection>,
        maxWidth: Float,
    ): Float = coerceIn(
        if (allowedDirections.contains(DismissDirection.Start)) -maxWidth else 0f,
        if (allowedDirections.contains(DismissDirection.End)) maxWidth else 0f
    )

    private fun Float.coerceHeightIn(
        allowedDirections: Set<DismissDirection>,
        maxHeight: Float,
    ): Float = coerceIn(
        if (allowedDirections.contains(DismissDirection.Up)) -maxHeight else 0f,
        if (allowedDirections.contains(DismissDirection.Down)) maxHeight else 0f,
    )

    /**
     * Finds the direction depending on which value is closest to its corresponding minValue
     * @return The direction of the dismiss action
     * or null if not the conditions for the dismiss have not been met
     */
    private fun getDismissDirection(
        valueX: Float,
        valueY: Float,
        minValueX: Float,
        minValueY: Float
    ): DismissDirection? {
        return listOf(
            DismissDirection.End to valueX / minValueX,
            DismissDirection.Start to valueX.absoluteValue / minValueX,
            DismissDirection.Down to valueY / minValueY,
            DismissDirection.Up to valueY.absoluteValue / minValueY,
        )
            .sortedBy { (_, ratio) -> ratio }
            .firstOrNull { (_, ratio) ->
                ratio >= 1
            }
            ?.first
    }
}

internal fun Modifier.dismissible(
    state: DismissibleState,
    directions: Set<DismissDirection>,
    enabled: Boolean = true,
    containerWidth: Dp,
    containerHeight: Dp,
    maxRotationZ: Float = 15f,
    velocityThreshold: Dp = 125.dp,
    minHorizontalProgressThreshold: Float = 0.7f,
    minVerticalProgressThreshold: Float = 0.7f,
) = composed {
    check(minHorizontalProgressThreshold > 0f && minHorizontalProgressThreshold <= 1) {
        "minHorizontalProgressToDismiss must be greater than 0 and less than or equal to 1"
    }
    check(minVerticalProgressThreshold > 0f && minVerticalProgressThreshold <= 1) {
        "minVerticalProgressToDismiss must be greater than 0 and less than or equal to 1"
    }

    val density = LocalDensity.current
    LaunchedEffect(directions, containerWidth, containerHeight) {
        state.directions = directions
        state.maxRotationZ = maxRotationZ
        state.minHorizontalProgressThreshold = minHorizontalProgressThreshold
        state.minVerticalProgressThreshold = minVerticalProgressThreshold
        with(density) {
            state.containerWidth = containerWidth.toPx()
            state.containerHeight = containerHeight.toPx()
            state.velocityThreshold = velocityThreshold.toPx()
        }
    }

    Modifier
        .pointerInput(directions, enabled) {
            if (!enabled) return@pointerInput
            coroutineScope {
                val velocityTracker = VelocityTracker()
                detectDragGestures(
                    onDragStart = { velocityTracker.resetTracking() },
                    onDragEnd = {
                        launch {
                            state.performFling(velocityTracker.calculateVelocity())
                        }
                    },
                    onDragCancel = {
                        launch {
                            state.onDismissCancel()
                            state.reset()
                        }
                    },
                    onDrag = { change: PointerInputChange, dragged: Offset ->
                        launch {
                            change.consume()
                            velocityTracker.addPosition(change.uptimeMillis, change.position)
                            state.performDrag(dragged)
                        }
                    })
            }
        }
        .graphicsLayer {
            translationX = state.value.x
            translationY = state.value.y
            rotationZ = state.rotationZ
        }
}

fun ModifierAnimateScope.dismissible(
    state: DismissibleState,
    directions: Set<DismissDirection>,
    enabled: Boolean = true,
    containerWidth: Dp,
    containerHeight: Dp,
    maxRotationZ: Float = 15f,
    velocityThreshold: Dp = 125.dp,
    minHorizontalProgressThreshold: Float = 0.7f,
    minVerticalProgressThreshold: Float = 0.7f,
){
    modifier = modifier.dismissible(state, directions, enabled, containerWidth, containerHeight, maxRotationZ, velocityThreshold, minHorizontalProgressThreshold, minVerticalProgressThreshold)
}

/**
 * @ExperimentalMaterialApi
 * @Composable
 * fun CardFeed(
 *     uiState: FeedUiState,
 *     onIntent: (FeedIntent) -> Unit,
 *     modifier: Modifier = Modifier
 * ) {
 *     BoxWithConstraints(modifier.fillMaxSize()) {
 *         val scope = rememberCoroutineScope()
 *
 *         val notDismissedItems = uiState.items.filterNot { it.isDismissed }
 *
 *         val preloadedItem = notDismissedItems.getOrNull(2)
 *         val backgroundItem = notDismissedItems.getOrNull(1)
 *         val topItem = notDismissedItems.getOrNull(0)
 *
 *         val topItemState = rememberDismissibleState(
 *             onDismiss = { direction ->
 *                 onIntent(
 *                     when (direction) {
 *                         DismissDirection.Start -> FeedIntent.Dislike(topItem!!) //TODO nullability
 *                         DismissDirection.End -> FeedIntent.Like(topItem!!) //TODO nullability
 *                         else -> throw IllegalArgumentException()
 *                     }
 *                 )
 *                 scope.launch {
 *                     reset(null)
 *                 }
 *             }
 *         )
 *
 *         PreloadFeedItem(
 *             item = preloadedItem,
 *             width = maxWidth,
 *             height = maxHeight
 *         )
 *
 *         if (backgroundItem != null) {
 *             FeedCard(
 *                 imageUrl = backgroundItem.imageUrl,
 *                 width = maxWidth,
 *                 height = maxHeight,
 *             )
 *         }
 *
 *         val topItemPainter = topItem?.imageUrl?.let { url ->
 *             FeedCardDefaults.rememberAsyncImagePainter(
 *                 imageUrl = url,
 *                 width = maxWidth,
 *                 height = maxHeight
 *             )
 *         }
 *         val isTopItemEnabled by remember(topItem) {
 *             derivedStateOf {
 *                 topItemPainter?.state is AsyncImagePainter.State.Success
 *             }
 *         }
 *
 *         FeedCard(
 *             painter = topItemPainter,
 *             modifier = Modifier.dismissible(
 *                 state = topItemState,
 *                 directions = if (isTopItemEnabled) {
 *                     setOf(DismissDirection.Start, DismissDirection.End)
 *                 } else {
 *                     emptySet()
 *                 },
 *                 enabled = isTopItemEnabled,
 *                 containerWidth = maxWidth,
 *                 containerHeight = maxHeight,
 *             )
 *         )
 *
 *         FeedButtons(
 *             topItemState = topItemState,
 *             enabled = isTopItemEnabled,
 *             modifier = Modifier.fillMaxSize()
 *         )
 *     }
 * }
 *
 * @Composable
 * private fun FeedButtons(
 *     topItemState: DismissibleState,
 *     enabled: Boolean,
 *     modifier: Modifier = Modifier,
 * ) = Box(modifier) {
 *     val scope = rememberCoroutineScope()
 *
 *     val dislikeButtonScale by remember {
 *         derivedStateOf {
 *             getButtonScale(topItemState.horizontalDismissProgress * -1)
 *         }
 *     }
 *     val likeButtonScale by remember {
 *         derivedStateOf {
 *             getButtonScale(topItemState.horizontalDismissProgress)
 *         }
 *     }
 *
 *     val animatedDislikeButtonScale by animateFloatAsState(dislikeButtonScale)
 *     val animatedLikeButtonScale by animateFloatAsState(likeButtonScale)
 *
 *     Row(
 *         Modifier
 *             .padding(bottom = 48.dp)
 *             .align(Alignment.BottomCenter),
 *         horizontalArrangement = Arrangement.SpaceBetween
 *     ) {
 *         var dismissAnimationJob by remember { mutableStateOf<Job?>(null) }
 *         FeedButton(
 *             onClick = {
 *                 scope.launch {
 *                     if (dismissAnimationJob?.isActive == true) return@launch
 *                     dismissAnimationJob = launch { topItemState.dismiss(DismissDirection.Start) }
 *                 }
 *             },
 *             modifier = Modifier.graphicsLayer {
 *                 scaleX = animatedDislikeButtonScale
 *                 scaleY = animatedDislikeButtonScale
 *             },
 *             enabled = enabled
 *         ) {
 *             DislikeIcon()
 *         }
 *         Spacer(Modifier.width(72.dp))
 *         FeedButton(
 *             onClick = {
 *                 scope.launch {
 *                     if (dismissAnimationJob?.isActive == true) return@launch
 *                     dismissAnimationJob = launch { topItemState.dismiss(DismissDirection.End) }
 *                 }
 *             },
 *             modifier = Modifier.graphicsLayer {
 *                 scaleX = animatedLikeButtonScale
 *                 scaleY = animatedLikeButtonScale
 *             },
 *             enabled = enabled
 *         ) {
 *             LikeIcon()
 *         }
 *     }
 * }
 *
 * @Composable
 * private fun FeedButton(
 *     onClick: () -> Unit,
 *     modifier: Modifier = Modifier,
 *     enabled: Boolean = true,
 *     content: @Composable () -> Unit,
 * ) {
 *     OutlinedButton(
 *         onClick = onClick,
 *         modifier = modifier.size(80.dp),
 *         enabled = enabled,
 *         shape = CircleShape,
 *         colors = ButtonDefaults.outlinedButtonColors(
 *             containerColor = Color.Black.copy(alpha = 0.3f),
 *             contentColor = Color.White,
 *         ),
 *         border = BorderStroke(
 *             width = 2.dp,
 *             color = Color.White,
 *         )
 *     ) {
 *         content()
 *     }
 * }
 *
 * @Composable
 * private fun DislikeIcon(modifier: Modifier = Modifier) {
 *     Icon(
 *         imageVector = Icons.Rounded.Close,
 *         contentDescription = stringResource(R.string.nope),
 *         modifier = modifier.size(32.dp),
 *         tint = Color.White
 *     )
 * }
 *
 * @Composable
 * private fun LikeIcon(modifier: Modifier = Modifier) {
 *     Icon(
 *         imageVector = Icons.Rounded.Favorite,
 *         contentDescription = stringResource(R.string.like),
 *         modifier = modifier.size(32.dp),
 *         tint = Color.White
 *     )
 * }
 *
 * private fun getButtonScale(dismissProgress: Float): Float {
 *     val minProgress = 0f
 *     val maxProgress = 0.5f
 *     val minScale = 0.8f
 *     val maxScale = 1f
 *
 *     return when {
 *         dismissProgress < minProgress -> minScale
 *         dismissProgress > maxProgress -> maxScale
 *         else -> dismissProgress.scale(
 *             oldMin = minProgress, oldMax = maxProgress,
 *             newMin = minScale, newMax = maxScale,
 *         )
 *     }
 * }
 */